#include "hal.h"
#include "devices/pic18/adc.h"

uint8_t hardwareVersionIndex = 0;

void hal_initialize(void) {
    NABTO_DEBUGC(("Hardware version index: %"PRIu8, hardwareVersionIndex));

    switch (hardwareVersionIndex) {
        case 0:
            TRISA = 0x20;
            LATA = 0x00;
            TRISB = 0xff;
            LATB = 0xc0;
            TRISC = 0xff;
            LATC = 0x01;
            TRISD = 0x03;
            LATD = 0x00;
            TRISE = 0x36;
            LATE = 0x00;
            TRISF = 0x7e;
            LATF = 0x00;
            TRISG = 0x00;
            LATG = 0x00;

            // Enable internal PORTB pull-ups
            INTCON2bits.RBPU = 1;

            adc_initialize(0x00 | 0b0011); // set analog input pins 0 - 11 to inputs, set Vss as Vref- and Vdd as Vref+.

            // timer2 config for PWM OUT
            PR2 = 0b11111111;
            T2CON = 0b00000111;
            break;

        case 1:
            TRISA = 0x28;
            LATA = 0x00;
            TRISB = 0xff;
            LATB = 0xc0;
            TRISC = 0xff;
            LATC = 0x01;
            TRISD = 0x06;
            LATD = 0x00;
            TRISE = 0x30;
            LATE = 0x00;
            TRISF = 0x7e;
            LATF = 0x00;
            TRISG = 0x10;
            LATG = 0x00;

            // Enable internal PORTB pull-ups
            INTCON2bits.RBPU = 1;

            adc_initialize(0x00 | 0b0011); // set analog input pins 0 - 11 to inputs, set Vss as Vref- and Vdd as Vref+.

            // timer2 config for PWM OUT
            PR2 = 0b11111111;
            T2CON = 0b00000111;
            break;
    }
}

/**
 * Toggle the onboard LED.
 * @return the new state of the LED.
 */
uint8_t toggle_led(void) {
    switch (hardwareVersionIndex) {
        case 0:
            LATDbits.LATD2 ^= 1;
            return LATDbits.LATD2 != 0;

        case 1:
            LATEbits.LATE0 ^= 1;
            return LATEbits.LATE0 != 0;
    }
}

/**
 * Read the current status of the onboard button.
 * @return true if the button is currently being pressed.
 */
uint8_t read_button_status(void) {
    switch (hardwareVersionIndex) {
        case 0:
        case 1:
            return PORTCbits.RC0 != 0;
    }
}

/**
 * Read analog input channel.
 * @param channel  channel input number - 0-5 for the external pins or 6 for the onboard temperature sensor.
 * @return the raw value read from the ADC.
 */
uint32_t read_analog_input(uint8_t channel) {
    uint32_t value;

    if (channel > 6) { // user inserted to big val
        return 0;
    } else {
        if (channel == 6) { // temp channel AN4
            channel = 4;
        } else { //A0 to A5: AN6 to AN11
            channel += 6;
        }

        ADCON0 &= ~(0b1111 << 2);
        ADCON0 |= channel << 2;

        ADCON0bits.GO = 1;
        while (ADCON0bits.GO);

        value = (uint32_t) ADRESH;
        value <<= 8ul;
        value |= (uint32_t) ADRESL;

        return value;
    }
}

/**
 * Read the temperature from the onboard temperature sensor
 * @return the temperature en degrees celcius.
 */
uint32_t read_temperature(void) {
    uint32_t t = 0;
    uint8_t i;

    for (i = 0; i < 8; i++) {
        t += read_analog_input(6);
    }
    t /= 8;

    t *= 211;
    t -= 32768;
    t /= 655;

    return t;
}

enum {
    HARDWARE_VERSION_0_4,
    HARDWARE_VERSION_1_2,
    HARDWARE_VERSIONS_SUPPORTED
};

#define NUMBER_OF_IO_PINS                                                               15

// look up tables for the io pins for the different revisions of the Nabduino board
near uint8_t * far rom ioTris[HARDWARE_VERSIONS_SUPPORTED][NUMBER_OF_IO_PINS] = {
    //  0       1       2       3       4       5       6       7       8       9      10      11      12      13      14
    {&TRISC, &TRISC, &TRISB, &TRISD, &TRISB, &TRISE, &TRISE, &TRISB, &TRISB, &TRISC, &TRISC, &TRISC, &TRISC, &TRISC, &TRISD}, // 0.4 beta
    {&TRISC, &TRISC, &TRISB, &TRISC, &TRISB, &TRISC, &TRISD, &TRISB, &TRISB, &TRISD, &TRISG, &TRISC, &TRISC, &TRISC, 0} // 1.2
};
near uint8_t * far rom ioLat[HARDWARE_VERSIONS_SUPPORTED][NUMBER_OF_IO_PINS] = {
    //  0      1      2      3      4      5      6      7      8      9     10     11     12     13     14
    {&LATC, &LATC, &LATB, &LATD, &LATB, &LATE, &LATE, &LATB, &LATB, &LATC, &LATC, &LATC, &LATC, &LATC, &LATD},
    {&LATC, &LATC, &LATB, &LATC, &LATB, &LATC, &LATD, &LATB, &LATB, &LATD, &LATG, &LATC, &LATC, &LATC, 0}
};
near uint8_t * far rom ioPort[HARDWARE_VERSIONS_SUPPORTED][NUMBER_OF_IO_PINS] = {
    //  0       1       2       3       4       5       6       7       8       9      10      11      12      13      14
    {&PORTC, &PORTC, &PORTB, &PORTD, &PORTB, &PORTE, &PORTE, &PORTB, &PORTB, &PORTC, &PORTC, &PORTC, &PORTC, &PORTC, &PORTD},
    {&PORTC, &PORTC, &PORTB, &PORTC, &PORTB, &PORTC, &PORTD, &PORTB, &PORTB, &PORTD, &PORTG, &PORTC, &PORTC, &PORTC, 0}
};
far rom uint8_t ioMask[HARDWARE_VERSIONS_SUPPORTED][NUMBER_OF_IO_PINS] = {
    //  0       1       2       3       4       5       6       7       8       9      10      11      12      13      14      // replace "1 << 0" with "1 /**/" to avoid compiler warnings and keep width.
    {1 << 7, 1 << 6, 1 << 3, 1 /**/, 1 << 1, 1 << 2, 1 << 1, 1 << 2, 1 /**/, 1 << 1, 1 << 2, 1 << 5, 1 << 4, 1 << 3, 1 << 1},
    {1 << 7, 1 << 6, 1 << 3, 1 << 2, 1 << 1, 1 << 1, 1 << 1, 1 << 2, 1 /**/, 1 << 2, 1 << 4, 1 << 5, 1 << 4, 1 << 3, 0 /**/} // only 14 IOs on version 1.2 onwards so null the last one
};

uint8_t io_config(uint8_t index, uint8_t io_out) {
    uint8_t status = io_out;
    uint8_t mask;

    mask = ioMask[hardwareVersionIndex][index];

    if (mask == 0) {
        return status;
    }

    switch (index) {
        case 3:
            set_pwm(0, false, 0);
            break;
        case 5:
            set_pwm(1, false, 0);
            break;
        case 6:
            set_pwm(2, false, 0);
            break;
        case 9:
            set_pwm(3, false, 0);
            break;
        case 10:
            set_pwm(4, false, 0);
            break;
    }

    // set direction
    if (io_out & 0x08) { // input
        *ioTris[hardwareVersionIndex][index] |= mask;
    } else { // output
        *ioTris[hardwareVersionIndex][index] &= ~mask;
    }

    // write value
    if (io_out & 0x01) { // write high
        *ioLat[hardwareVersionIndex][index] |= mask;
    } else { // write low
        *ioLat[hardwareVersionIndex][index] &= ~mask;
    }

    // read back pin value
    if (*ioPort[hardwareVersionIndex][index] & mask) { // read back high
        status |= 0x10;
    } else { // read back low
        status &= ~0x10;
    }

    return status;
}

bool set_pwm(uint8_t channel, bool enable, uint8_t level) {
    switch (hardwareVersionIndex) {
        case 0:
            switch (channel) {
                case 3: // RC1/ECCP2
                    if (enable) {
                        TRISCbits.TRISC1 = 0;
                        LATCbits.LATC1 = 0;
                        CCP2CON = 0x0c;
                        CCPR2L = level;
                    } else {
                        CCP2CON = 0x00;
                        CCPR2L = 0;
                    }
                    return true;

                case 4: // RC2/ECCP1
                    if (enable) {
                        TRISCbits.TRISC2 = 0;
                        LATCbits.LATC2 = 0;
                        CCP1CON = 0x0c;
                        CCPR1L = level;
                    } else {
                        CCP1CON = 0x00;
                        CCPR1L = 0;
                    }
                    return true;

                case 5: // RD1/ECCP3
                    if (enable) {
                        TRISDbits.TRISD1 = 0;
                        LATDbits.LATD1 = 0;
                        CCP3CON = 0x0c;
                        CCPR3L = level;
                    } else {
                        CCP3CON = 0x00;
                        CCPR3L = 0;
                    }
                    return true;
            }
            break;

        case 1:
            switch (channel) {
                case 0: // RC2/ECCP1
                    if (enable) {
                        TRISCbits.TRISC2 = 0;
                        LATCbits.LATC2 = 0;
                        CCP1CON = 0x0c;
                        CCPR1L = level;
                    } else {
                        CCP1CON = 0x00;
                        CCPR1L = 0;
                    }
                    return true;

                case 1: // RC1/ECCP2
                    if (enable) {
                        TRISCbits.TRISC1 = 0;
                        LATCbits.LATC1 = 0;
                        CCP2CON = 0x0c;
                        CCPR2L = level;
                    } else {
                        CCP2CON = 0x00;
                        CCPR2L = 0;
                    }
                    return true;

                case 2: // RD1/ECCP3
                    if (enable) {
                        TRISDbits.TRISD1 = 0;
                        LATDbits.LATD1 = 0;
                        CCP3CON = 0x0c;
                        CCPR3L = level;
                    } else {
                        CCP3CON = 0x00;
                        CCPR3L = 0;
                    }
                    return true;

                case 3: // RD2/CCP4
                    if (enable) {
                        TRISDbits.TRISD2 = 0;
                        LATDbits.LATD2 = 0;
                        CCP4CON = 0x0c;
                        CCPR4L = level;
                    } else {
                        CCP4CON = 0x00;
                        CCPR4L = 0;
                    }
                    return true;

                case 4: // RG4/CCP5
                    if (enable) {
                        TRISGbits.TRISG4 = 0;
                        LATGbits.LATG4 = 0;
                        CCP5CON = 0x0c;
                        CCPR5L = level;
                    } else {
                        CCP5CON = 0x00;
                        CCPR5L = 0;
                    }
                    return true;
            }
            break;
    }

    return false;
}
